/*
 * Singleton.java
 *
 * Created on July 22, 2007, 10:21 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 *
 * There can only be one instance of this class. It also holds references to many of the objects that will be used by many other objects
 * in FileCopier so they can all be accessed from a common place.
 */
package filecopier;

import java.io.File;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.awt.Image;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Result;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.swing.JFrame;
import jargs.gnu.CmdLineParser;
import org.apache.log4j.Logger;
import org.apache.log4j.Level;
import org.apache.log4j.Appender;
import org.apache.log4j.xml.DOMConfigurator;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Attr;
import custom.util.FileCopy;
import filecopier.options.Options;

/**
 *
 * @author david
 */
public class Singleton {

    private static int FILE_LOGGER = 0;
    private static int FILE_ALWAYS_LOGGER = 1;
    private static int CONSOLE_LOGGER = 2;
    private static int FILE_CONSOLE_LOGGER = 3;
    private static int GUI_LOGGER = 4;
    private static int FILE_GUI_LOGGER = 5;
    private static Singleton _instance = null;
    private static Image filecopierIcon16 = null;
    private static Image filecopierIcon32 = null;
    private Options options = null;
    private static Logger[] loggers = null;
    private boolean dirsInitted = false;
    private Document log4jConfig = null;
    private boolean log4jChanged = false;
    private File log4jFile = null;
    private JFrame frame = null;
    private String appVersion = "1.5.0";

    /** Creates a new instance of Singleton */
    private Singleton() {
    }

    public static Singleton getInstance() {
        if (_instance == null) {
            synchronized (Singleton.class) {
                if (_instance == null) {
                    _instance = new Singleton();
                }
            }
        }
        return _instance;
    }

    public void init(String[] args) throws FileCopierException, CmdLineParser.OptionException {
        try {
            initFileCopierDirs();
            //initLoggers();
            initOptions(args);
            initLoggers();
            filecopierIcon16 = createImage("images/FileCopier16.png");
            filecopierIcon32 = createImage("images/FileCopier32.png");
        } catch (IOException e) {
            throw new FileCopierException(e, e);
        }
    }

    private String initHomeDir() throws IOException {
    
        System.setProperty("filecopier.home", System.getProperty("user.home") +
                System.getProperty("file.separator") + "filecopier");
        File homeDir = new File(System.getProperty("filecopier.home"));
        if ((!homeDir.exists()) && (!homeDir.mkdir())) {
            throw new IOException(homeDir + "directory could not be created.");
        }

        return homeDir.getPath();
    }

    private String initScriptsDir() throws IOException {
        System.setProperty("filecopier.scripts", initHomeDir() + System.getProperty("file.separator") + "Scripts");
        File scriptsDir = new File(System.getProperty("filecopier.scripts"));
        if ((!scriptsDir.exists()) && (!scriptsDir.mkdir())) {
            throw new IOException(scriptsDir + "directory could not be created.");
        }

        return scriptsDir.getPath();
    }

    private String initDefaultCopyScriptsDir() throws IOException {
        System.setProperty("filecopier.scripts.copy", System.getProperty("filecopier.scripts") +
                System.getProperty("file.separator") + "CopyScripts");
        File defaultCopyScriptsDir = new File(System.getProperty("filecopier.scripts.copy"));
        if ((!defaultCopyScriptsDir.exists()) && (!defaultCopyScriptsDir.mkdir())) {
            throw new IOException(defaultCopyScriptsDir + "directory could not be created.");
        }
        return defaultCopyScriptsDir.getPath();
    }

    private String initDefaultRestoreScriptsDir() throws IOException {
        System.setProperty("filecopier.scripts.restore", initScriptsDir() +
                System.getProperty("file.separator") + "RestoreScripts");
        File defaultRestoreScriptsDir = new File(System.getProperty("filecopier.scripts.restore"));
        if ((!defaultRestoreScriptsDir.exists()) && (!defaultRestoreScriptsDir.mkdir())) {
            throw new IOException(defaultRestoreScriptsDir + "directory could not be created.");
        }
        return defaultRestoreScriptsDir.getPath();
    }

    private String initTempScriptsDir() throws IOException {
        System.setProperty("filecopier.scripts.temp", System.getProperty("filecopier.scripts") +
                System.getProperty("file.separator") + "TempScripts");
        File tempScriptsDir = new File(System.getProperty("filecopier.scripts.temp"));
        if ((!tempScriptsDir.exists()) && (!tempScriptsDir.mkdir())) {
            throw new IOException(tempScriptsDir + "directory could not be created.");
        }

        return tempScriptsDir.getPath();
    }

    public void initFileCopierDirs() throws IOException {
        if (dirsInitted == false) {
            initDefaultRestoreScriptsDir();
            initDefaultCopyScriptsDir();
            initTempScriptsDir();
            dirsInitted = true;
        }
    }

    public void initOptions(String[] args) throws FileCopierException, CmdLineParser.OptionException {
        if (options == null) {
            options = new Options(args);
        }
    }

    public void initLoggers() throws FileCopierException {
        if (loggers == null) {
            log4jConfig = initLog4jConfig();
            initLoggerLevels();
            DOMConfigurator.configure(log4jConfig.getDocumentElement());
            loggers = new Logger[FILE_GUI_LOGGER + 1];
            loggers[FILE_LOGGER] = Logger.getLogger("filecopier.file.FileCopier");
            loggers[FILE_ALWAYS_LOGGER] = Logger.getLogger("filecopier.file.always.FileCopier");
            loggers[CONSOLE_LOGGER] = Logger.getLogger("filecopier.console.FileCopier");
            loggers[FILE_CONSOLE_LOGGER] = Logger.getLogger("filecopier.file.console.FileCopier");
            loggers[GUI_LOGGER] = Logger.getLogger("filecopier.gui.FileCopier");
            loggers[FILE_GUI_LOGGER] = Logger.getLogger("filecopier.file.gui.FileCopier");
        }
    }

    public void initLoggerLevels() {
        Level level = options.getLogLevel();
        if (options.didLogLevelChange() && level != null) {
            setLoggerLevels(level);
            log4jChanged = true;
            options.logLevelUpdated();
        }
    }

    // This sets the level of the appenders in the log4j file. It is called at shutdown.
    public void setLoggerLevels(Level level) {
        String levelStr = level.toString();
        Element root = log4jConfig.getDocumentElement();
        NodeList appenders = root.getElementsByTagName("appender");
        for (int i = 0; i < appenders.getLength(); i++) {
            Node appender = appenders.item(i);
            NodeList params = appender.getChildNodes();
            for (int j = 0; j < params.getLength(); j++) {
                Node param = params.item(j);
                if (param.getNodeName().equals("param")) {
                    boolean thresholdParamFound = false;
                    NamedNodeMap atts = param.getAttributes();
                    for (int k = 0; k < atts.getLength(); k++) {
                        Attr attr = (Attr) atts.item(k);
                        if (attr.getName().equals("name") && attr.getValue().equals("threshold")) {
                            thresholdParamFound = true;
                        }
                        if (thresholdParamFound && attr.getName().equals("value")) {
                            thresholdParamFound = false;
                            // If the appender is the "always" appender, don't change the level because it is set up to always log to
                            // the log file, no matter what the user sets the level to.
                            if (!getAppenderName(appender).equals("always")) {
                                // If this appender is not the file appender, the threshold can be higher than INFO.
                                if ((level.equals(Level.WARN) || level.equals(Level.ERROR)) && !getAppenderName(appender).equals("file")) {
                                    // The console/gui appender's level will be set no higher than INFO.
                                    attr.setValue("INFO");
                                } else {
                                    attr.setValue(levelStr);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    // This sets the levels of the appenders in memory, but not in the log4j.xml file.
    public void setLoggerLevels() {
        Level level = options.getLogLevel();
        setAppenderLevels(level);
        log4jChanged = true;
    }

    // This sets the levels of the appenders in memory, but not in the log4j.xml file.
    public void setAppenderLevels(Level level) {
        // There are three appenders, the file appender, the console appender and the JTextArea appender.
        // Getting the appenders for the FILE_CONSOLE_LOGGER and the GUI_LOGGER will get all three appenders.

        // The FILE_CONSOLE_LOGGER logs to the console appender and the file appender.
        java.util.Enumeration<Appender> appenders = loggers[FILE_CONSOLE_LOGGER].getAllAppenders();
        if (appenders != null) {
            while (appenders.hasMoreElements()) {
                Appender a = appenders.nextElement();
                if (a.getName().equals("file")) {
                    org.apache.log4j.RollingFileAppender rfa = (org.apache.log4j.RollingFileAppender) a;
                    rfa.setThreshold(level);
                } else if (a.getName().equals("stdout")) {
                    org.apache.log4j.ConsoleAppender ca = (org.apache.log4j.ConsoleAppender) a;
                    ca.setThreshold(level.isGreaterOrEqual(Level.INFO) ? Level.INFO : level);
                }
            }
        }

        // The GUI_LOGGER logs to the JTextAreaAppender appender.
        appenders = loggers[GUI_LOGGER].getAllAppenders();
        if (appenders != null) {
            while (appenders.hasMoreElements()) {
                Appender a = appenders.nextElement();
                if (a.getName().equals("JTextAreaAppender")) {
                    custom.log4j.JTextAreaAppender taa = (custom.log4j.JTextAreaAppender) a;
                    taa.setThreshold(level.isGreaterOrEqual(Level.INFO) ? Level.INFO : level);
                }
            }
        }
    }

    public Level getAppenderLevel(final String appenderName) {
        Level level = Level.INFO;

        if (appenderName.equals("file")) {
            Appender a = loggers[FILE_LOGGER].getAppender(appenderName);
            if (a != null) {
                level = (Level) ((org.apache.log4j.RollingFileAppender) a).getThreshold();
            }
        } else if (appenderName.equals("stdout")) {
            Appender a = loggers[CONSOLE_LOGGER].getAppender(appenderName);
            if (a != null) {
                level = (Level) ((org.apache.log4j.ConsoleAppender) a).getThreshold();
            }
        } else if (appenderName.equals("JTextAreaAppender")) {
            Appender a = loggers[GUI_LOGGER].getAppender(appenderName);
            if (a != null) {
                level = (Level) ((custom.log4j.JTextAreaAppender) a).getThreshold();
            }
        }

        return level;
    }

    private String getAppenderName(Node appender) {
        String name = "";
        NamedNodeMap atts = appender.getAttributes();
        for (int i = 0; i < atts.getLength(); i++) {
            Attr attr = (Attr) atts.item(i);
            if (attr.getName().equals("name")) {
                name = attr.getValue();
            }
        }

        return name;
    }

    private Document initLog4jConfig() throws FileCopierException {
        Document log4jConfig = null;
        File log4jXml = initLog4jFile();
        try {
            //URL log4jDtd = Logger.class.getResource("xml/log4j.dtd");
            DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            db.setEntityResolver(new Log4jResolver());
            log4jConfig = db.parse(log4jXml);
        } catch (ParserConfigurationException pce) {
            throw new FileCopierException(pce, pce);
        } catch (SAXException sxe) {
            throw new FileCopierException(sxe, sxe);
        } catch (IOException ioe) {
            throw new FileCopierException(ioe, ioe);
        }
        return log4jConfig;
    }
    
    private File initLog4jFile() {
        // See if the log4j.xml file is in filecopier's home directory.
        log4jFile = new File(System.getProperty("filecopier.home") + System.getProperty("file.separator") + "log4j.xml");
        if (!log4jFile.exists()) {
            // The log4j.xml file does not exist in filecopier's home directory, so copy the log4j.xml file in 
            // FileCopier.jar to filecopier's home directory.
            getLog4jFileFromJar();
        }
        return log4jFile;
    }
    
    
    private File getLog4jFileFromJar() {
        InputStream is = Singleton.class.getResourceAsStream("log4j.xml");
        try {
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            FileWriter fw = new FileWriter(log4jFile);
            try {
                String line = null;
                String sep = System.getProperty("line.separator");
                while((line = br.readLine()) != null) {
                    fw.write(line + sep);
                }
            } finally {
                br.close();
                isr.close();
                is.close();
                fw.close();
            }
        } catch(IOException e) {
            // We were unable to create a log4j.xml file in the user's home dir for filecopier, so use
            // the log4j.xml file in the jar.
            System.out.println("Could not create log4j.xml in home dir");
            log4jFile = new File(Singleton.class.getResource("log4j.xml").getFile());
        }
        return log4jFile;
    }

    private static Image createImage(final String path) {
        Image image = null;
        java.net.URL imgURL = Singleton.class.getResource(path);
        if (imgURL != null) {
            image = java.awt.Toolkit.getDefaultToolkit().getImage(imgURL);
        } else {
            System.err.println("Couldn't find file: " + path);
        }
        return image;
    }

    public Image getImage16() {
        return filecopierIcon16;
    }
    
    public Image getImage32() {
        return filecopierIcon32;
    }
    
    public String getAppVersion() {
        return appVersion;
    }

    public Logger getFileLogger() {
        return (loggers != null ? loggers[FILE_LOGGER] : null);
    }

    public Logger getFileAlwaysLogger() {
        return (loggers != null ? loggers[FILE_ALWAYS_LOGGER] : null);
    }

    public Logger getConsoleLogger() {
        return (loggers != null ? loggers[CONSOLE_LOGGER] : null);
    }

    public Logger getFileConsoleLogger() {
        return (loggers != null ? loggers[FILE_CONSOLE_LOGGER] : null);
    }

    public Logger getGuiLogger() {
        return (loggers != null ? loggers[GUI_LOGGER] : null);
    }

    public Logger getFileGuiLogger() {
        return (loggers != null ? loggers[FILE_GUI_LOGGER] : null);
    }

    public Logger getLogger(int idx) {
        Logger logger = null;
        if (idx >= FILE_LOGGER && idx <= GUI_LOGGER) {
            logger = loggers[idx];
        }
        return logger;
    }

    public JFrame getFrame() {
        return frame;
    }

    public Options getOptions() {
        return options;
    }

    public boolean didLog4jFileChange() {
        return log4jChanged;
    }

    public void setFrame(JFrame frame) {
        this.frame = frame;
    }

    public void saveLog4jFile() {
        if (log4jChanged == true) {
            setLoggerLevels(options.getLogLevel());
            try {
                Transformer transformer = TransformerFactory.newInstance().newTransformer();
                // The following two lines are necessary for the xml data to be indented in the file.
                transformer.setOutputProperty(OutputKeys.INDENT, "yes");
                transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");

                Source src = new DOMSource(log4jConfig);
                Result res = new StreamResult(log4jFile);
                transformer.transform(src, res);
            } catch (TransformerException e) {
                loggers[FILE_LOGGER].error("Failure to copy changed log4j config file to \"" + log4jFile + "\".");
            }
        }
    }

    private class Log4jResolver implements EntityResolver {

        public InputSource resolveEntity(String publicId, String systemId) {
            InputSource is = null;
            if (systemId != null && systemId.endsWith("log4j.dtd")) {
                java.io.InputStream in = Logger.class.getResourceAsStream("xml/log4j.dtd");
                is = new InputSource(in);
            }
            return is;
        }
    }
}
